using System;
using System.Collections.Generic;
using System.IO;
using System.Text;

namespace DeflateLib
{
    /// <summary>
    /// Contains some GZip (RFC1952) utils for reading/writing 
    /// the format data.
    /// 
    /// To read data: ReadGZipHeader, decomress body, WriteCrcAndISize and check them.
    /// 
    /// To write data: WriteGZipHeader, compress body, WriteCrcAndISize for the data.
    /// </summary>
    public static class GZipUtils
    {
        const byte ID1 = 31, ID2 = 139;
        const byte TextFlag = 0x01;
        const byte HcrcFlag = 0x02;
        const byte ExtraFlag = 0x04;
        const byte NameFlag = 0x08;
        const byte CommentFlag = 0x10;

        public const UInt32 Crc32Start = 0;

        public static void WriteGZipHeader(Stream stream, GZipHeader header)
        {
            if (stream == null) throw new ArgumentNullException("stream");
            if (header == null) throw new ArgumentNullException("header");
            if (!IsValidString(header.Name)) throw new ArgumentException("Invalid Name field in info");
            if (!IsValidString(header.Comment)) throw new ArgumentException("Invalid Comment field in info");

            int xlen = 0;
            if (header.Extra != null)
            {
                int sum = 0;
                for (int i = 0; i < header.Extra.Length; i++)
                {
                    if(header.Extra[i].Data == null) throw new ArgumentException("Invalid extra data in info");
                    int itemLength = header.Extra[i].Data.Length;
                    if(itemLength > 0xFFFF) throw new ArgumentException("One of data items in info is too big");
                    sum = checked(sum + 4 + itemLength);
                }
                if(sum > 0xFFFF) throw new ArgumentException("Data in info is too big");
                xlen = sum;
            }          

            byte flags = 0;
            if (header.Text != null) flags |= TextFlag;
            if (header.Crc16.HasValue) flags |= HcrcFlag;
            if (header.Extra != null) flags |= ExtraFlag;
            if (header.Name != null) flags |= NameFlag;
            if (header.Comment != null) flags |= CommentFlag;

            byte[] fixedHeader = new byte[] {
                ID1, ID2, header.CompressMethod, flags, 
                (byte)(header.ModificationTime), (byte)(header.ModificationTime >> 8),
                (byte)(header.ModificationTime >> 16), (byte)(header.ModificationTime >> 24),
                header.ExtraFlags, header.OS };

            stream.Write(fixedHeader, 0, fixedHeader.Length);

            if (header.Extra != null)
            {
                stream.WriteByte((byte)xlen);
                stream.WriteByte((byte)(xlen >> 8));
                for (int i = 0; i < header.Extra.Length; i++)
                {
                    stream.WriteByte(header.Extra[i].SI1);
                    stream.WriteByte(header.Extra[i].SI2);
                    byte[] data = header.Extra[i].Data;
                    stream.WriteByte((byte)data.Length);
                    stream.WriteByte((byte)(data.Length >> 8));
                    stream.Write(data, 0, data.Length);                    
                }
            }
            if(header.Name != null)
            {
                byte[] data = ToZeroTerminated(header.Name);
                stream.Write(data, 0, data.Length);
            }

            if (header.Comment != null)
            {
                byte[] data = ToZeroTerminated(header.Comment);
                stream.Write(data, 0, data.Length);
            }

            if (header.Crc16.HasValue)
            {
                stream.WriteByte((byte)header.Crc16.Value);
                stream.WriteByte((byte)(header.Crc16.Value >> 8));
            }
        }

        public static void WriteCrcAndISize(Stream stream, UInt32 crc, UInt32 iSize)
        {
            if (stream == null) throw new ArgumentNullException("stream");

            byte[] data = new byte[] {
                (byte)(crc), (byte)(crc >> 8),
                (byte)(crc >> 16), (byte)(crc >> 24),
                (byte)(iSize), (byte)(iSize >> 8),
                (byte)(iSize >> 16), (byte)(iSize >> 24) };
            stream.Write(data, 0, data.Length);
        }

        private static byte[] ToZeroTerminated(string s)
        {
            byte[] data = new byte[s.Length + 1];
            for (int i = 0; i < s.Length; i++)
            {
                data[i] = (byte)s[i];
            }
            return data;
        }

        private static bool IsValidString(string s)
        {
            if (String.IsNullOrEmpty(s)) return true;
            for (int i = 0; i < s.Length; i++)
            {
                if (s[i] == '\x00' || s[i] > '\x7F') return false;
            }
            return true;
        }

        public static GZipHeader ReadGZipInfo(Stream stream)
        {
            if (stream == null) throw new ArgumentNullException("stream");

            const int FixedHeaderSize = 10;
            byte[] fixedHeader = ReadExact(stream, FixedHeaderSize);
            if (fixedHeader[0] != ID1 || fixedHeader[1] != ID2)
                throw new GZipException("Invalid GZIP header ID");

            GZipHeader info = new GZipHeader();
            info.CompressMethod = fixedHeader[2];
            info.ExtraFlags = fixedHeader[8];
            info.ModificationTime = (uint)fixedHeader[4] | ((uint)fixedHeader[5] << 8) |
                ((uint)fixedHeader[6] << 16) | ((uint)fixedHeader[7] << 24);
            info.OS = fixedHeader[9];

            byte flags = fixedHeader[3];
            if ((flags & ExtraFlag) != 0)
            {
                List<GZipExtra> extra = new List<GZipExtra>();
                byte[] data = ReadExact(stream, 2);
                int extraLength = data[0] | (data[1] << 8);
                int read = 0;
                while (read < extraLength)
                {
                    data = ReadExact(stream, 4);
                    read += 4;
                    GZipExtra extraItem = new GZipExtra();
                    extraItem.SI1 = data[0];
                    extraItem.SI2 = data[1];
                    int itemLength = data[2] | (data[3] << 8);
                    if (read + itemLength > extraLength)
                        throw new GZipException("Invalid GZIP extra data");
                    extraItem.Data = ReadExact(stream, itemLength);
                    extra.Add(extraItem);
                    read += itemLength;
                }
                info.Extra = extra.ToArray();
            }

            if ((flags & NameFlag) != 0)
            {
                info.Name = ReadZeroTerminated(stream);
            }

            if ((flags & CommentFlag) != 0)
            {
                info.Comment = ReadZeroTerminated(stream);
            }

            if ((flags & HcrcFlag) != 0)
            {
                byte[] data = ReadExact(stream, 2);
                info.Crc16 = (ushort)(data[0] | (data[1] << 8));
            }

            return info;
        }

        private static byte[] ReadExact(Stream stream, int length)
        {
            if (length == 0) return new byte[0];

            byte[] data = new byte[length];
            int read = stream.Read(data, 0, length);
            if (read != length) throw new GZipException("Unexpected end of stream");
            return data;
        }

        private static string ReadZeroTerminated(Stream stream)
        {
            StringBuilder sb = new StringBuilder();
            int ch = stream.ReadByte();
            while (ch != 0)
            {
                if (ch < 0) throw new GZipException("Unexpected end of stream");
                sb.Append((char)ch);
                ch = stream.ReadByte();
            }
            return sb.ToString();
        }

        public static void ReadCrcAndISize(Stream stream, out UInt32 crc, out UInt32 iSize)
        {
            if (stream == null) throw new ArgumentNullException("stream");

            byte[] data = ReadExact(stream, 8);
            crc = (uint)data[0] | ((uint)data[1] << 8) |
                ((uint)data[2] << 16) | ((uint)data[3] << 24);
            iSize = (uint)data[4] | ((uint)data[5] << 8) |
                ((uint)data[6] << 16) | ((uint)data[7] << 24);
        }

        public static uint Crc(byte[] data)
        {
            return Crc(data, 0, data.Length);
        }

        public static uint Crc(byte[] data, int index, int length)
        {
            return Crc(data, index, length, Crc32Start);
        }

        public static uint Crc(byte[] data, int index, int length, uint crc)
        {
            uint c = crc ^ 0xFFFFFFFF;
            for (int n = 0; n < length; n++) {
              c = CrcTable[(c ^ data[index++]) & 0xff] ^ (c >> 8);
            }
            return c ^ 0xFFFFFFFF;
        }

        static uint[] CrcTable;

        static GZipUtils()
        {
            // compute CrcTable
            CrcTable = new uint[256];
            for (int n = 0; n < 256; n++)
            {
                uint c = (uint)n;
                for (int k = 0; k < 8; k++)
                {
                    if ((c & 1) != 0)
                    {
                        c = 0xEDB88320 ^ (c >> 1);
                    }
                    else
                    {
                        c = c >> 1;
                    }
                }
                CrcTable[n] = c;
            }
        }
    }

    public class GZipException : Exception
    {
        public GZipException(string message)
            : base(message)
        {
        }
    }
}
